/*
 * Copyright (c) 2012 Johan Fylling
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights 
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

part of engine;


abstract class Integrator {
  void integrate (Particle particle, num drag, num timeSquared);
}


class StaticIntegrator extends Integrator {
  num timeSquared;
  
  StaticIntegrator (num stepSizeMs) {
    num stepSizeS = stepSizeMs / 1e3;
    this.timeSquared = stepSizeS*stepSizeS;
  }
  
  void integrate (Particle particle, num drag, num stepSize) {
    Vector position = particle.position;
    Vector acceleration = particle.force * particle.massInv;
    Vector velocity = (position - particle.positionPrev) + acceleration * timeSquared;
    particle.positionPrev = position;
    particle.position = position + velocity * drag;
    particle.force = new Vector2();
  }
}


class VariableIntegrator extends Integrator {
  void integrate (Particle particle, num drag, num stepSize) {
  }
}


class ParticleEngine implements PhysicsEngine {
  final Integrator integrator;
  final int        _stepSize;
  num              drag;
  Vector           _gravity;
  
  /**
   * Constructor.
   * 
   * @param stepSize the time in ms taken each simulation step.
   * @param drag the amount of intrinsic drag, where 0=total drag and 1=no drag.
   * @param gravity the direction and amplitude of simulation gravity.
   */
  ParticleEngine (Integrator this.integrator, this._stepSize, num this.drag, Vector gravity) {
    // Convert step size from ms to s.
    num deltaTime = this._stepSize / 1e3;
    this._gravity = (gravity != null ? gravity / deltaTime : new Vector2(0,0));
  }
  
  void step (num deltaTime, WorldState ws) {
    if (ws is! ParticleWorldState) {
      return;
    }
    
    for (ParticleSystem ps in ws.activeParticleSystems) {
      for (Particle p in ps.particles) {
        p.force = p.force + this._gravity * p.mass;
        this.integrator.integrate(p, this.drag, deltaTime);
      }
      
      for (int i = 0; i < 3; i++) {
        for (Constraint c in ps.constraints) {
          c.satisfy();
        }
      }
      
      if (ps.velocity.lengthSquared > 0.01) {
        ps.activityCounter = DynamicBody.activityCounterBase;
      } else {
        ps.activityCounter--;
        if (ps.activityCounter <= 0) {
          ws.activeParticleSystems.remove(ps);
        }
      }
    }
  }
  
  void activate (WorldState ws, DynamicBody body) {
    body.activityCounter = DynamicBody.activityCounterBase;
    
    if (ws is! ParticleWorldState || body is! ParticleSystem) {
      return;
    }
    
    if (ws.particleSystems.contains(body)) {
      ws.activeParticleSystems.add(body);
    }
  }
  
  void pushBody (WorldState ws, DynamicBody body, Vector2 push, [Vector2 point]) {
    if (body == null || body is! ParticleSystem) {
      return;
    }
    
    if (!?point) {
      point = new Vector2();
    }
    
    Vector2 normal = push.normalized();
    
    List<num> distances = new List<num>();
    num max = 0.0;
    num min = 0.0;
    bool firstPass = true;
    for (Particle p in body.particles) {
      num distance = normal.perpDot((p.position as Vector2) - point);
      distances.addLast(distance.abs());
      if (firstPass) {
        firstPass = false;
        max = min = distance;
      } else if (distance > max) {
        max = distance;
      } else if (distance < min) {
        min = distance;
      }
    }
    
    num span = max - min;
    
    int i = 0;
    for (Particle p in body.particles) {
      num offset = push.length * (1.0 - (span == 0.0 ? 0.0 : distances[i] / span));
      p.position = p.position + normal * offset;
      i++;
    }
    
    activate(ws, body);
  }
  
  void forceBody (WorldState ws, DynamicBody body, Vector2 force, [Vector2 point]) {
    if (body == null || body is! ParticleSystem) {
      return;
    }
    
    if (!?point) {
      point = new Vector2();
    }
    
    Vector2 normal = force.normalized();
    
    List<num> distances = new List<num>();
    num max = 0.0;
    num min = 0.0;
    bool firstPass = true;
    for (Particle p in body.particles) {
      num distance = normal.perpDot((p.position as Vector2) - point);
      distances.addLast(distance.abs());
      if (firstPass) {
        firstPass = false;
        max = min = distance;
      } else if (distance > max) {
        max = distance;
      } else if (distance < min) {
        min = distance;
      }
    }
    
    num span = max - min;
    
    int i = 0;
    for (Particle p in body.particles) {
      num offset = force.length * (1.0 - (span == 0.0 ? 0.0 : distances[i] / span));
      p.force = p.force + normal * offset;
      i++;
    }
    
    activate(ws, body);
  }
}
